#define AF_INET			2

#define SOCK_STREAM		1
#define SOCK_RAW		2

#define LOCAL_IP		"LocalIP"
#define LOCAL_PORT		"LocalPort"
#define REMOTE_IP		"RemoteIP"
#define REMOTE_PORT		"RemotePort"
#define CONNECT_MODE	"ConnectMode"
#define HANDLER 		"Handler"

#define EVENT_STATE_CHANGE		0
#define EVENT_DATA_ARRIVED		1


Script CRaw
(name, config_option)

String LocalIP = "0.0.0.0";
String RemoteIP = "127.0.0.1";

String Name = name;
U4 LocalPort = 0, RemotePort = 0;
U2 ConnectMode = 0;					//0 Passive, 1 Active
String Handler = "DefaultHandler";

I4 Status = 0, LogEnable = 1;
U0 EID = 0;
Object Log, Sock, Thread;
ObjCreateEx(Log, "QLog");
ObjCreateEx(Sock, "QSock");
ObjCreateEx(Thread, "QThread");

if (GetCount(Parameter) == 2)
{
	Construct(config_option);
}
else
{
	Construct();
}

Script Construct
(config_option)
String tmp;
I4 ret;

if (GetCount(Parameter))
{
	if (GetActualType(config_option) == "IMap")
	{

	}
	elseif (config_option)
	{
		SetConfig("Log file name", StrAssign(tmp, "Log/", Name, "_%Y%M%D_%h%m.txt"));
		SetConfig("Log time format", "%Z ");
		ret = QSO_Start(Log);
		if (ret)
		{
			PrintLn("Failed to start log at ", GetConfig("Log file name"));
		}
		else
		{
			oLog("Log file started");
		}
	}
}
EndScript

Script SetConfig
(key, value)
I4 ret = -1;
switch (key)
{
case "LogFileName"
case "Log file name"
case "Log time format"
	ret = QSO_Set(Log, key, value);
	break;

case "LocalPort"
case "ConnectMode"
case "RemoteIP"
case "RemotePort"
case "Handler"
	QS_GetMember(key) = value;
	ret = 0;
	break;
}

if (ret)
{
	oLog("[Error] Failed to SetConfig for ", key, " to ", value);
}
else
{
	oLog("Setting ", key, " to ", value);
}

EndScript

Script GetConfig
(key)
String value;
switch (key)
{
case "LogFileName"
case "Log file name"
case "Log time format"
	QSO_Get(Log, key, value);
	return value;

case "LocalPort"
case "ConnectMode"
case "RemoteIP"
case "RemotePort"
	return QS_GetMember(key);

default
	return "";
}
EndScript

Script Start
I4 ret;
if (LogEnable)
{
	ret = QSO_Start(Log);
}

if (EID)
{
	oLog("[Error] Already started");
	return;
}

EID = PCall("TransportThread");
if (EID == 0)
{
	oLog("[Error] Failed to start TransportThread");
	return -1;
}

EndScript


Script TransportThread
I4 ret;
Object v;
U4 revent;

U4 t0, tdiff;

ThreadPost(Thread, Handler, Name, $EVENT_STATE_CHANGE, 1, Status);
Status = 1;

Again:
ret = StartSock();
switch (ret)
{
case 0 			//connected
	ThreadPost(Thread, Handler, Name, $EVENT_STATE_CHANGE, 2, Status);
	Status = 2;
	if (Comm())		//if user stop (1), disconnected (0)
	{
		break;		//user stop
	}
	ThreadPost(Thread, Handler, Name, $EVENT_STATE_CHANGE, 1, Status);
	Status = 1;
	//fall through

case 1 			//failed to connect
	PrintLn("Waiting for T5 before reconnecting");
	oLog("Waiting for T5 before reconnecting");
	t0 = GetTick();
	WaitAgain:
	tdiff = GetTick() - t0;
	ret = QSO_Wait(Sock, 0, ifexp(tdiff < 25000)(25000 - tdiff)(0), revent);
	if (ret)
	{
		oLog("[Error] QSO_Wait returned ", ret, " at TransportThread");
		goto Again;
	}

	if (revent & 2)
	{
		if (TaskGetEx(0, v) == 0)
		{
			if (v[0] == "Stop")
			{
				oLog("User stop in ", ScriptName());
				return 2;
			}
			oLog("[Error] Discarded cmd ", v[0], " at ", ScriptName());	
		}
		goto WaitAgain;
	}

	if (revent & 1)
	{
		PrintLn("T5 expired");
		oLog("T5 expired");
		goto Again;
	}
	
	oLogError("TransportThread unhandled revent ", revent);
	goto Again;

default
	oLog("[Error] TransportThread unhandled code ", ret);
	break;

case 2 			//user quit
	oLog("TranportThread User quit");
	//fall through
}

EID = 0;
ThreadPost(Thread, Handler, Name, $EVENT_STATE_CHANGE, 0, Status);
Status = 0;
EndScript

Script Send
(s)
oLog("[Cmd] Send ", StrLen(s), " bytes");
TaskPost(EID, "Send", s);
EndScript

Script ShutDown
TaskPost(EID, "ShutDown");
EndScript

Script Stop
TaskPost(EID, "Stop");
EndScript


Script HelperClose
oLog("Closing socket");
I4 ret = QSO_Close(Sock);
if (ret)
{
	oLogError("[Error] QSO_Close return ", ret);
}
EndScript

Script Comm

//return 1 if user stop
//return 0 if disconnected

I4 ret;
Object v;
String s;
U4 sent_length;
U4 ShutDownFlag = 0;
U4 revent;

oLog("TCP_Worker is up, eid = ", QS_EID(), ", tid = ", Sys_TID());

while (1)
{
	ret = QSO_Wait(Sock, 20, 3000, revent);			//4 for Read, 16 for hangup
	
	if (revent & 1)		//timeout during wait
	{}
	
	if (revent & 2)		//User event
	{
		oLog("Received user event, QSO_ResetUserEvent returned ", QSO_ResetUserEvent(Sock));

		if (TaskGetEx(0, v) == 0 && GetCount(v))
		{
			switch (v[0])
			{
			case "Stop"
				oLog("Received stop");
				HelperClose();
				return 1;
			
			case "Send"
				oLog("Sending ", StrLen(v[1]), " bytes\x0A", v[1]);
				ret = QSO_Send(Sock, v[1], sent_length);
				if (ret)
				{
					oLog("Failed to send, ret is ", ret);
				}
				else
				{
					oLog(sent_length, " bytes sent");
					if (sent_length != StrLen(v[1]))
					{
						oLog("[Error] todo need multiple write");
					}
				}
				break;
				
			case "ShutDown"
				ShutDownFlag = 1;
				ret = QSO_ShutDown(Sock, 1);				//shutdown sending
				break;
				
			default
				oLog("[Error] Unknown cmd, ", v[0]);
				break;
			}
		}
	}
	
	if (revent & 4)
	{
		ret = QSO_Recv(Sock, s, 65530);
		if (ret)
		{
			oLog("[Error] QSO_RecvFailed failed ", ret);
			HelperClose();
			return 0;
		}
		
		if (StrLen(s) == 0)
		{
			oLog("[Error] Remote shutdown ", ret);
			HelperClose();
			return 0;
		}

		oLog("Received ", StrLen(s), " bytes\x0A", StrHex(s));
		ThreadPost(Thread, Handler, Name, $EVENT_DATA_ARRIVED, s);
	}
	
	if (revent & 8)		//write event
	{}
	
	if (revent & 16)		//hangup event
	{
		oLog("Remote hangup");
		while (1)
		{
			ret = QSO_Recv(Sock, s, 65530);
			if (ret)
			{
				oLog("[Error] QSO_Recv after hangup event returned ", ret);
				HelperClose();
				return 0;
			}
			
			if (StrLen(s))
			{
				oLog("Received ", StrLen(s), " bytes\x0A", s);
			}
			else
			{
				oLog("No more data is in buffer");
				HelperClose();
				return 0;
			}
		}
	}
	
	if (revent & 32)		//accept event (Windows only)
	{}
	
	if (revent & 64)		//connect event (Windows only)
	{}
}
EndScript

Script oLog
String s;
Reference p;

foreach(p, Parameter)
{
	StrAppend(s, p);
}

if (QSO_WriteLn(Log, s))
{
	PrintLn(s);
}

EndScript

Script oLogError
String s = "[Error] ";
Reference p;

foreach(p, Parameter)
{
	StrAppend(s, p);
}

if (QSO_WriteLn(Log, s))
{
	PrintLn(s);
}

EndScript

Script StartSock
//0 if connected, 1 if failed to connect, 2 user stop

I4 ret;
String ip, port;

ret = QSO_Socket(Sock, $AF_INET, $SOCK_STREAM, 0);			//TCP
if (ret)
{
	oLogError("QSO_Socket failed at StartSock, ", ret);
	return 1;
}

oLog("QSO_Socket success");

oLog("SetBlocking result is ", QSO_SetBlocking(Sock, 0));

ret = QSO_Bind(Sock, LocalIP, LocalPort);
if (ret)
{
	oLogError("QSO_Bind failed at StartSock, ", ret);
	HelperClose();
	return 1;
}

oLog("QSO_Bind success");

ret = QSO_GetSockName(Sock, ip, port);
if (ret)
{
	oLogError("QSO_GetSockName failed at ", ScriptName(), ", ", ret);
	HelperClose();
	return 1;
}

oLog("Bind is at ", ip, ":", port);

return ifexp(ConnectMode)(Active())(Passive());

EndScript

Script Active
I4 ret;
U4 revent;
Object v;
String ip, port;

oLog("Attempting to connect to ", RemoteIP, ":", RemotePort);
ret = QSO_Connect(Sock, RemoteIP, RemotePort);
switch (ret)
{
case 10035
	oLog("Windows WSA_WOULD_BLOCK");
	ActiveWaitAgain:
	ret = QSO_Wait(Sock, 4 | 8 | 16 | 32 | 64, 60000, revent);
	if (ret)
	{
		oLogError("QSO_Wait failure " ,ret);
		HelperClose();
		return 1;
	}

	if (revent & 64)		//connect is succesful
	{
		break;
	}

	if (revent & 2)
	{
		oLog("Received user event at Active, QSO_ResetUserEvent returned ", QSO_ResetUserEvent(Sock));
		if (TaskGetEx(0, v) == 0)
		{
		 	if (v[0] == "Stop")
		 	{
		 		oLog("User stop in ", ScriptName());
		 		HelperClose();
				return 2;
		 	}
			oLog("[Error] Discarded cmd ", v[0], " at ", ScriptName());	
		}
		goto ActiveWaitAgain;
	}

	oLog("Unexpected event at Active ", revent);
	HelperClose();
	return 1;

case 0
	break;

default
	oLog("QSO_Connect failed, ", ret);
	HelperClose();
	return 1;
}

oLog("Connection is succesful");

ret = QSO_GetSockName(Sock, ip, port);
if (ret)
{
	oLogError("QSO_GetSockName failed at ", ScriptName(), ", ", ret);
	HelperClose();
	return 1;
}

oLog("Communicating socket is at ");

return 0;
EndScript

Script Passive

//return 2 user stop, 1 failed to establish, 0 if establish
U4 revent;
Object v;
Object x;
String ip;
U4 port;
I4 ret = QSO_Listen(Sock, 1);
if (ret)
{
	oLogError("QSO_Listen failed, ", ret);
	HelperClose();
	return 1;
}

oLog("QSO_Listen is succesful");

WaitAgain:
ret = QSO_Wait(Sock, LinWin(4, 32), revent);			//wait forever
if (ret)
{
	oLog("[Error] QSO_Wait at Passive returns ", ret);
	HelperClose();
	return 1;
}

if (revent & 2)		//User event
{
	oLog("Received user event at Passive, QSO_ResetUserEvent returned ", QSO_ResetUserEvent(Sock));
	if (TaskGetEx(0, v) == 0)
	{
		if (v[0] == "Stop")
		{
			oLog("User stop in ", ScriptName());
			HelperClose();
			return 2;
		}
		oLog("[Error] Discarded cmd at ", ScriptName());
	}
}
	
if (revent & 4 || revent & 32)
{
	ObjCreateEx(x, "QSock");
	ret = QSO_Accept(Sock, x, ip, port);
	if (ret)
	{
		oLogError("QSO_Accept failed ", ret);
		HelperClose();
		return 1;
	}
	oLog("Socket accepted from ", ip, ":", port);

	ObjExchange(Sock, x);
	
	ret = QSO_GetSockName(Sock, ip, port);
	if (ret)
	{
		oLogError("QSO_GetSockName failed at ", ScriptName(), ", ", ret);
		HelperClose();
		return 1;
	}
	oLog("Communicating socket is at ", ip, ":", port);
	return 0;
}

oLog("WaitAgain at Passive");
goto WaitAgain;

EndScript

EndScript

Script StartActive
(name, ip, port, handler)
GCreateEx(name, "CRaw", name, 1);
:name.SetConfig($CONNECT_MODE, 1);
:name.SetConfig($REMOTE_IP, ip);
:name.SetConfig($REMOTE_PORT, port);
:name.SetConfig($HANDLER, ifexp(GetCount(Parameter) >= 4)(handler)("DefaultHandler"));
:name.Start();

EndScript

Script StartPassive
(name, port, handler)
GCreateEx(name, "CRaw", name, 1);
:name.SetConfig($LOCAL_PORT, port);
:name.SetConfig($HANDLER, ifexp(GetCount(Parameter) >= 3)(handler)("DefaultHandler"));
:name.Start();
EndScript

Script ShutDown
(name)
:name.ShutDown();
EndScript

Script DefaultHandler
(name, event, s)
(name, event, new_state, old_state)

PrintLn(Parameter);

switch (event)
{
case 0
	PrintLn(name, " state change from ", old_state, " to ", new_state);
	break;

case 1
	PrintLn(name, " received ", StrLen(s), " bytes");
	HandleUpgrade(name, s);
	break;
}
EndScript

Script TTT
(name)
:name.oLog("XXXXX");
EndScript

Script SendResource
(name, r)
String s;
if (GetResource(r, s))
{
	PrintLn("Failed to GetResource for ", r);
	return;
}
:name.Send(s);

EndScript

Resource Hello
Hello world
EndResource

Script Start
(name)
:name.Start();
EndScript

Script Stop
(x)
:x.Stop();
EndScript

Script StartP
(name)
GCreateEx(name, "CRaw", name, 1);
EndScript

Script StopP
(name)
GDelete(name);
EndScript

Script XSendResource
(name, r, n)
U0 loop = n;
while (loop)
{
	SendResource(name, r);
	Dec(loop);
}
EndScript

Script SendRaw
(name, s)
:name.Send(s);
EndScript

Script HandleUpgrade
(name, t)

String key;
String sha, base64;
U0 pos;
PrintLn(t);
String reply = "HTTP/1.1 101 Switching Protocols\x0D\x0A";
if (StrFind(t, "Sec-WebSocket-Key: ", 0, pos))
{
	StrReplace(t, 0, pos + 19, "");			//remove all the unwanted header
	if (StrFind(t, "\x0D\x0A", 0, pos))
	{
		StrSubEx(key, t, 0, pos);
		PrintLn(key);
		if (StrSHA1(sha, StrAppend(key, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")))
		{
			StrToBase64(base64, sha);
			PrintLn(base64);
			StrAppend(reply, "Upgrade: websocket\x0D\x0A");
			StrAppend(reply, "Connection: Upgrade\x0D\x0A");
			StrAppend(reply, "Sec-WebSocket-Accept: ", base64, "\x0D\x0A");
			StrAppend(reply, "Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=12; client_max_window_bits=12\x0D\x0A");
			StrAppend(reply, "Date: Tue, 30 Jan 2024 10:45:22 GMT\x0D\x0A");
			StrAppend(reply, "Server: Python/3.9 websockets/12.0\x0D\x0A\x0D\x0A");
			PrintLn(reply);
			SendRaw(name, reply);
		}
		else
		{
			PrintLn("Failed to calculate SHA1");
		}
	}
	else
	{
		PrintLn("Could not get key");
	}
}
else
{
	PrintLn("Failed to get Sec-WebSocket-Key");
}
EndScript